大家好,本系列挑戰文希望可以把 JS 稍微進階一些的技術原理、React 的進階常用應用、以及一些工程開發上會遇到的狀況,如重構、design pattern等,由淺入深的介紹一遍。幫助有需要提升 JS 能力的各位新手工程師,包含我自己,免於害怕有朝一日會被 AI 給取代的威脅!
我去年想寫 Three.js 結果失敗了 XD 只撐了五天,希望這一次 可以多幾天 能夠順利盡量把 30 天的份量寫完!!
~以下正文開始~
💡本篇主題與重點字:**Execution Context**
- Scope Chain
- Hoisting
本週的主題將著重於 JavaScript 的一些奇幻魔法,在剛開始學的時候可能不會注意到的細節,或是一不小心就錯過學習的核心機制!筆者第一個學ㄉ程式語言是 C++,JS 總是邊做 project 邊學,因此直到現在有時寫 JS 或做 code review 的時候還是會「蛤?這樣也可以?!」。
希望透過介紹 Execution Context、閉包、原型鏈、event loop、functional programming特性 等基礎,讓讀者能夠有足量的能力離開新手村!
今天介紹的 Execution Context 是其中一個 JavaScript 的核心機制,因為會把一層層 JS 執行的最小單位 frame push 進 stack,因此也叫做 stack frame。EC 這個機制將會影響與決定
⋯⋯若不知道以上的內容也請先不要走開,今天的文章將一一解釋 😉 (後來發現篇幅太長了,因此今天只有提到前面兩點,後半段下回繼續)
當我們開始執行 JavaScript 時,每段程式碼都是在某個 EC 中進行的。無論是函數呼叫、全域程式碼(Global Code)、或 eval ,都會創建自己的 Execution Context。每個 EC 會有:
var 、函式宣告let or const
this Binding
eval是一個 JS 的內建函數,可以執行字串形式的 JS script。
JavaScript 是靜態作用域語言,意思是變數的作用域在編譯時就決定了,取決於程式碼的書寫位置,而不是根據呼叫位置。當執行時,要查找變數,JS 引擎只會從當前 EC 的 Lexical Environment 查找。以下提供一個簡單的例子:宣告一個 function 並在內部宣告另一個 function,讓我們來觀察模擬 JS 是如何逐步 trace down:

當程式開始執行時,首先建立 Global EC。在最一開始的 建立階段(Creation Phase) 建立全域物件、變數以及函數:
- 全域物件(window or global),以及 this
- 變數有 a,初始化為 undefined
- 函數有 outer,初始化為 function object
接著,在 執行階段(Execution Phase) 給變數賦值
- a = 10
EC Stack 裡此時只有 Global EC
[Global EC]
outer() → 創建 Outer EC當我們呼叫 outer() 後,便把 outer EC push 進 EC stack,所以此時的 EC stack 有 Global 和 Outer stack 兩個。此步驟同樣的會先有 建立階段:
- 變數 b → undefined
- 函數 inner → function object
執行階段:
- b = 20
- 呼叫 inner(),建立 Inner EC
EC Stack 已經依序被 push 進 Outer EC 和 Inner EC
[Inner EC]
[Outer EC]
[Global EC]
inner() → 創建 Inner ECc → undefined
c = 30
console.log(a, b, c)
這邊的 console.log會根據 stack 裡面的順序,一一沿著作用域鏈 (Scope Chain) 層層往下查找變數的值:
a → 在 Inner 沒有 → Outer 沒有 → Global 找到 10
b → Inner 沒有 → Outer 找到 20
c → Inner 找到 30
得到最終的輸出:
10 20 30
總結上述的例子,詞法作用域 (Lexical Scope) 就是在函數定義時記住外部環境,不管函數被怎麼調用,它始終會沿著「定義時的作用域鏈」向上查找變數。
Hoisting(提升)是 JavaScript 中的一種行為機制,指的是 變數宣告(var)和函式宣告會被「提升」到其所在作用域的最前面。 無論把變數或函式宣告寫在程式碼的哪裡,JavaScript 會先在執行之前把這些宣告「往上移動」。
1. 函式提升(Function Hoisting)
函式宣告會被整個提升,包含函式本體。

2. 變數提升(Variable Hoisting)
使用 var 宣告的變數會被提升,但只有宣告本身會被提升,初始化不會。像是以下範例中 var a 的宣告被提升到了程式碼的最上方,但 a = 10 的賦值還是在原本的位置。因此,變數在宣告之前被使用時,值是 undefined,而不是錯誤。

let 和 const 不會被提升到可用狀態
let 和 const 也會被引擎知道,但在程式碼執行到宣告之前,變數處於暫時性死區(Temporal Dead Zone, TDZ),會拋出錯誤,跟上面的栗子相比,這種寫法可以幫助我們更好偵錯,最終維持程式的品質與可預測性。這正是為什麼現代 JavaScript 開發中建議盡量用 let 和 const而避免用 var。
var 的四大理由1. var 的 Hoisting 行為容易讓人誤解var 宣告的變數會被提升,但初始化不會,導致在宣告前使用變數時得到 undefined,這很容易讓人誤會變數已經有值,實際卻不是。這會增加 Debug 難度,也容易造成潛在的程式錯誤。
2. var 只有函式作用域,沒有塊級作用域(block scope)
var 在函式內宣告後,會在整個函式內都有效,即使在 {} 內宣告也沒意義,容易造成變數覆寫、污染。相比之下,let 和 const 有塊級作用域,只在 {} 內有效,更容易控制變數範圍。
3. let 和 const 有 Temporal Dead Zone (TDZ)
使用 let 或 const 在變數宣告之前訪問會拋錯,幫助提早發現錯誤,讓程式碼更安全。
4. const 明確表達不變的意圖
用 const 宣告的變數不可以被重新賦值,讓變數使用更具語意。
所以我們把前面查找變數的例子再次拿出來,不知道讀者剛剛有沒有注意到,例子中宣告變數的方法是 var 而非平常較常用的 let 和 const。如果我們改為以下

雖然這邊的結果會是一樣的,不過細節上有些許差異:
1. 作用域let 和 const 是塊級作用域(block scope),作用範圍是最近的大括號 {} 內:這裡因為 a 是全域,b 在 outer 函式內,c 在 inner 內,所以行為才會和 var 一樣。
2. Hoisting 與暫時性死區 (Temporal Dead Zone, TDZ)var 會提升 (hoist)變數宣告,初始化為 undefined ;let 和 const 也會被提升,但不會初始化,而是進入 TDZ,直到程式碼執行到該行才會被賦值。但這邊沒有先呼叫才賦值的情況,所以安全過關~
當我們認識了 EC 這個機制跟他的衍生小夥伴後,寫出來的程式將更穩定、更能被正確預測。最後,讓我們來列點總結今天提到的內容:
JavaScript execution model | MDN
感謝 未知作者 的精彩分享!
JavaScript 生態系統真的很豐富,這樣的分享對開發者很有幫助。
實際的程式碼範例很有幫助,讓理論更容易理解。
遇到的問題和解決方案分享很實用,相信很多人都會遇到類似的情況。
也歡迎版主有空參考我的系列文「南桃AI重生記」:https://ithelp.ithome.com.tw/users/20046160/ironman/8311
如果覺得有幫助的話,也歡迎訂閱支持!